Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding slack integration for posting to channel on applies. Fixes #179 #180

Closed

Conversation

nicholas-wu-hs
Copy link
Contributor

@nicholas-wu-hs nicholas-wu-hs commented Nov 2, 2017

Tackling #179, first iteration using slack-token and slack-channel in the config.
If specified, a Slack Client will be created and attached to ApplyExecutor. It's used to post messages to slack-channel on applies (both success and fail).

Using https://github.com/nlopes/slack for slack client.

Will probably need a lot of refactoring/restructuring, then eventually tests and docs?
Also, was I supposed to add the dependencies into the repo?

server/server.go Outdated
@@ -61,6 +64,14 @@ func NewServer(config Config) (*Server, error) {
return nil, err
}
githubStatus := &events.GithubStatus{Client: githubClient}
// nil slackClient unless token and channel was specified
var slackClient slack.Client
if config.SlackToken != "" && config.SlackChannel != "" {
Copy link
Contributor Author

@nicholas-wu-hs nicholas-wu-hs Nov 2, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only attempt to create a slack client if both token and channel are supplied.
If we decide to only use a token and channel, I plan to add a warning for when the user supplies ONLY one item. But the schema will probably be changed after discussion in #179 and this'll be irrelevant.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In general it's dangerous to use nil for something like this because it's not obvious that my slack client will be nil just because somewhere up the chain depending on the config. On the other hand, you can't initialize a slack client if you don't have the config :D.

I would create a NoopSlackClient that implements the interface but doesn't do anything and use it here if there is no slack config.

@@ -106,3 +114,19 @@ func (a *ApplyExecutor) apply(ctx *CommandContext, repoDir string, plan models.P

return ProjectResult{ApplySuccess: output}
}

func createSlackMessage(ctx *CommandContext, success bool) string {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this functionality doesn't belong in the apply executor since it's all about applying, not creating slack messages. Put this in the slack client.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

don't use package functions like this, instead add it to the struct.
func (a *ApplyExecutor) createSlackMessage

If you don't do this then anyone can call this method from anywhere in this package which is weird, you really only want it called from the object

status = ":x:"
}

return fmt.Sprintf("%s *%s* %s in <%s|%s>.",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you add a screenshot so we can see what this looks like?

Copy link
Contributor Author

@nicholas-wu-hs nicholas-wu-hs Nov 2, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here it is for using the emojis!
screen shot 2017-11-02 at 4 27 40 pm

Copy link
Contributor Author

@nicholas-wu-hs nicholas-wu-hs Nov 2, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also had them as text at one point:
screen shot 2017-11-02 at 4 41 27 pm

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And as attachments but they're chunkier:
screen shot 2017-11-02 at 4 42 09 pm

}

// https://api.slack.com/faq
// 'How do I find a channel's ID if I only have its #name?'
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think you need this. It says you can just use the name: https://api.slack.com/methods/chat.postMessage#channels

Copy link
Contributor Author

@nicholas-wu-hs nicholas-wu-hs Nov 3, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With chat.postMessage you can use a channel name, but if the channel doesn't exist, it doesn't send.

I was thinking of making sure the channel from config exists on the creation of the slack client and failing early if it doesn't, rather than failing at chat.postMessage or failing silently.

So to do that, I looked at https://api.slack.com/methods/channels.info which requires an ID. Alternatively, I can do a chat.postMessage and see if it succeeds but that would post a message? Or there might be other ways!

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the idea of failing when Atlantis server starts if that channel doesn't exist. Failing early is usually better so good call.

}
}

return nil, errors.New("channel_not_found")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

errors should be human readable so you could have just had "channel doesn't exist"

server/server.go Outdated
@@ -61,6 +64,14 @@ func NewServer(config Config) (*Server, error) {
return nil, err
}
githubStatus := &events.GithubStatus{Client: githubClient}
// nil slackClient unless token and channel was specified
var slackClient slack.Client
if config.SlackToken != "" && config.SlackChannel != "" {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In general it's dangerous to use nil for something like this because it's not obvious that my slack client will be nil just because somewhere up the chain depending on the config. On the other hand, you can't initialize a slack client if you don't have the config :D.

I would create a NoopSlackClient that implements the interface but doesn't do anything and use it here if there is no slack config.

@nickwu241
Copy link

nickwu241 commented Nov 4, 2017

Thanks for the awesome review and feedback!

  • Restructured to be more generic and handle Webhook config schema discussed at Slack integration on apply #179
  • Fixed errors to be human readable
  • Refactored slack related items to the server/events/slack
  • Reverted 3be7f8c (adding vendor deps) for now so it's easier to review

TODO:

  • Proper error handling and messages
  • Check if slack channel exists on server startup and error early if it doesn't
  • ?Default workspace-regex to ".*" if not provided
  • ?Pass command success/failure status down to HookExecutors
  • Add tests

server/server.go Outdated
for _, webhook := range config.Webhooks {
hookExecutor, err := hookExecutorBuilder.build(webhook)
if err != nil {
// TODO: check if it was because of uninitialized slack client
Copy link

@nickwu241 nickwu241 Nov 4, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If slack-token wasn't provided in the config, hookExecutorBuilder would have a nil slack.Client. This will cause an error when building slack webhooks, then Atlantis can error saying the user needs to provide a slack-token.

But this doesn't seem too clean, any suggestions? Using a NoopClient would work, but if we can error early and tell the user what's wrong, I feel like that's the way to go!

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In my suggestions I have a WebhooksManager that has a []WebhookSender. When it gets constructed with no webhooks then it just has an empty array and so calling WebhooksManager.Send() just does nothing. That's how I handled it.

When it's running through the webhooks in its constructor, it returns an error if a slack webhook was specified but there was no slack-token: https://github.com/nicholas-wu-hs/atlantis/pull/1/files?diff=unified#diff-0250d5b43682815402ad30ce1b068cdcR55

matched, err := regexp.MatchString(workspaceRegex, ctx.Command.Environment)
if err != nil {
// log the regexp error but let's continue with the apply
ctx.Log.Debug(err.Error())
Copy link

@nickwu241 nickwu241 Nov 4, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we can verify the regex during the config processing stage?
Does a debug message make sense here? Or should it be an error :O

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we can verify the regex during the config processing stage?

exactly regexp.Compile

@lkysow
Copy link
Collaborator

lkysow commented Nov 6, 2017

I'll add specific comments to your code but in general here's the changes I think need to be made and why:

  • lets move everything under an events/webhooks package. I don't think slack needs its own package at the moment and then all our code can be under that package cleanly including hook_executor
  • I want the code in ApplyExecutor to be as clean as possible and not need to know anything about webhooks. The ideal call would be just be
	a.WebhooksManager.Send(webhookResult)
  • constructing all the webhooks clients and verifying the regex, etc. should happen inside the WebhooksManager constructor. So in server/server.go it should just look like
	webhookManager, err := webhooks.NewWebhooksManager(webhooksConfig, config.SlackToken)
	if err != nil {
		return nil, errors.Wrap(err, "initializing webhooks")
	}
  • the interfaces I'd like to see are
    • a WebhooksSender that handles sending out multiple webhooks. This is what the ApplyExecutor has a reference to.
type WebhooksSender interface {
	Send(log *logging.SimpleLogger, result ApplyResult)
}
  • a WebhookSender that handles sending out a single webhook. This can either be implemented by the SlackSender or a GenericSender. The WebhooksSender will have a reference to multiple WebhookSender's. These will decide based on the environment whether to send out their webhooks or not.
type WebhookSender interface {
	Send(ApplyResult) error
}

I've messed around with the code and created a pr back into your branch for what I'd like to see: nicholas-wu-hs#1. Feel free to just use that as a guide if you want to code it out yourself.

server/server.go Outdated
Channel string `mapstructure:"channel"`
}

type HookExecutorBuilder struct {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would all of this code out of the server package and into events/webhooks. Ask yourself if building a HookExecutor should be the responsibility of the server class

Copy link
Contributor Author

@nicholas-wu-hs nicholas-wu-hs Nov 8, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, I thought about this too.
Previously, I tried to pass the the config structs from the server package to the server/events/webhooks code and ran into circular import problems, googled a bit for more info but found no clear solution.

In general, does a .go file import packages from parent directories?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no it doesn't because of those circular import errors you ran into. You can either create a separate package (like models) or you only reference from parent to child, never from child to parent

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it, thank you 👍

server/server.go Outdated
Slack slack.Client
}

func (builder *HookExecutorBuilder) build(webhook Webhook) (events.HookExecutor, error) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pointer receivers should just be the first letter of the struct, in this case h *HookExecutorBuilder.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it now! Thanks, will do this for all future code!

server/server.go Outdated
for _, webhook := range config.Webhooks {
hookExecutor, err := hookExecutorBuilder.build(webhook)
if err != nil {
// TODO: check if it was because of uninitialized slack client
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In my suggestions I have a WebhooksManager that has a []WebhookSender. When it gets constructed with no webhooks then it just has an empty array and so calling WebhooksManager.Send() just does nothing. That's how I handled it.

When it's running through the webhooks in its constructor, it returns an error if a slack webhook was specified but there was no slack-token: https://github.com/nicholas-wu-hs/atlantis/pull/1/files?diff=unified#diff-0250d5b43682815402ad30ce1b068cdcR55

server/server.go Outdated
@@ -86,6 +142,7 @@ func NewServer(config Config) (*Server, error) {
}
applyExecutor := &events.ApplyExecutor{
Github: githubClient,
WSRegexToHook: applyExecutorRegexToHook,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

here's where I would have liked to see something like a WebhooksManager so the ApplyExecutor only needs to call a single line of code to send webhooks

server/server.go Outdated
Webhooks []Webhook `mapstructure:"webhooks"`
}

type Webhook struct {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd call this WebhookConfig since it's no an actual webhook, just config for it.

In my suggestions I don't pass this in directly to the WebhooksManager. This is because I want the config that users need in their config files to be decoupled from the WebhooksManager. This will prevent us from accidentally changing the name of a config option in the WebhooksManager code and invalidating everyone's config files :) We'll have to explicitly do the mapping: https://github.com/nicholas-wu-hs/atlantis/pull/1/files?diff=unified#diff-91bbeda7eb98a7adc57b9e47e2cf5c2bR72


// execute apply webhooks
for workspaceRegex, hookExecutor := range a.WSRegexToHook {
matched, err := regexp.MatchString(workspaceRegex, ctx.Command.Environment)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if you had constructed the regex during the construction of the slack webhook sender then here you could have a reference to *regexp.Regexp and call .MatchString() which won't return an error.

Copy link
Contributor Author

@nicholas-wu-hs nicholas-wu-hs Nov 8, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome, thank you!

client *slack.Client
}

type SlackMessageTemplate struct {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good call to have this as a struct rather than multiple params

}, nil
}

func (s *ConcreteClient) PostMessage(channel string, template SlackMessageTemplate) (string, error) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

here you should have (c *ConcreteClient)

return timestamp, err
}

func (s *ConcreteClient) createMessage(template SlackMessageTemplate) string {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I want to do the attachment style message that you showed earlier. I think that looks best.

Copy link
Contributor Author

@nicholas-wu-hs nicholas-wu-hs Nov 8, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed that it looks nicer, one downside is that it takes a bit more room (1 attachment is at min ~3 lines), I'll play around a bit more with the message style.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah it takes up more room but I think it's worth it because it allows the information to be shown in a more consumable manner

matched, err := regexp.MatchString(workspaceRegex, ctx.Command.Environment)
if err != nil {
// log the regexp error but let's continue with the apply
ctx.Log.Debug(err.Error())
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we can verify the regex during the config processing stage?

exactly regexp.Compile

@nicholas-wu-hs
Copy link
Contributor Author

nicholas-wu-hs commented Nov 10, 2017

Still working on the tests, looking into using pegomock, I'll let you know when this is ready for another review :)

So this is another shot at using slack attachments, what do you think?

  • is Environment important enough to keep as its own field?

  • do we need a Status field? it's encoded in the colors but might not be clear
    screen shot 2017-11-10 at 9 58 05 am

  • if we want the attachment to be smaller, we can do something like Applied <ENV> in <REPO> by <USER>. or <USER> applied <ENV> in <REPO>.
    screen shot 2017-11-10 at 10 07 01 am

@@ -92,6 +94,16 @@ func (a *ApplyExecutor) apply(ctx *CommandContext, repoDir string, plan models.P
env := ctx.Command.Environment
tfApplyCmd := append(append(append([]string{"apply", "-no-color"}, applyExtraArgs...), ctx.Command.Flags...), plan.LocalPath)
output, err := a.Terraform.RunCommandWithVersion(ctx.Log, absolutePath, tfApplyCmd, terraformVersion, env)

// Send webhooks.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In general comments should be used to

  1. explain why a piece of code is doing what it is
  2. explain what a large section of code is doing if it's not clear from just reading it

For 2. we should strive to write code that removes the need to comment it because it's so clear.

In this case, you've done that, it's pretty obvious what a.Webhooks.Send is doing and so the comment Send webhooks. doesn't tell me any more information.

The problem with comments is that they're a form of duplication and they're harder to keep up to date. If I rename Send() to Queue() then my code won't compile if I don't change the function everywhere, but your comment might get left behind and fall out of date.

tl;dr: remove this comment, it adds nothing

Copy link
Contributor Author

@nicholas-wu-hs nicholas-wu-hs Nov 14, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the insight, I agree with the view on what comments should be and developers should strive to write self-documenting code -- I'll will remove the comment :P

return nil, errors.Wrap(err, "testing slack authentication")
}

// Make sure the slack channel exists.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a good comment. I'd add
Make sure the slack channel exists so we can error early.

func (s *SlackWebhook) createAttachments(result ApplyResult) []slack.Attachment {
var color string
if result.Success {
color = "good"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

make these constants at the top of file underneath the import section:

const successColour = "good"
const failureColour = "danger"

(also color => colour because we're canadian :D 🇨🇦 )

package webhooks_test

// todo: actually test
// purposefully empty to trigger coverage report
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice work figuring this out

configs := validConfigs()
configs[0].WorkspaceRegex = invalidRegex
_, err := webhooks.NewWebhooksManager(configs, validToken)
Assert(t, strings.Contains(err.Error(), "error parsing regexp"), "expected err")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if err is nil then calling err.Error() will cause a panic in the test

e.Error()
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x20 pc=0x1094fa6]

This is fine right now because your code is working but if someone breaks the code then the tests will be hard to figure out what's wrong because they will just have a panic. So I'd add an extra test here

Assert(t, err != nil, "expected error")

And then if err is not nil you'll get an easier to debug failure.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That makes sense, now I know and will nil check errors in the tests now & for the future, ty!

configs := validConfigs()
configs[0].Kind = unsupportedKind
_, err := webhooks.NewWebhooksManager(configs, validToken)
Assert(t, strings.Contains(err.Error(), "kind"), "expected err")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use Equals and check the whole error string

Channel: validChannel,
}

func validConfigs() []webhooks.Config {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is this doing? Why are you appending an empty slice to validConfig? Why not just have

var validConfig = []webhooks.Config{
     {Event: ...}
}

Copy link
Contributor Author

@nicholas-wu-hs nicholas-wu-hs Nov 14, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What I wanted to do was get a fresh copy of the valid configs for each test since I modify it in some tests.
So I think after having validConfig as a variable per your example, I can do config := validConfig in my tests to copy it.
Thanks!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I left it as a function because I want new copies of the valid configs in each test, but I got rid of the appending.

func TestNewWebhooksManager_NoSlackToken(t *testing.T) {
emptyToken := ""
_, err := webhooks.NewWebhooksManager(validConfigs(), emptyToken)
Assert(t, strings.Contains(err.Error(), "slack-token must be set"), "expected err")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should also test that if there are no configs then you don't need a slack-token

// todo: failing because not a valid token and channel
m, err := webhooks.NewWebhooksManager(validConfigs(), validToken)
Ok(t, err)
Assert(t, m != nil, "mangager shouldn't be nil")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mangager => manager

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks for the catch!

server/server.go Outdated
}

func NewServer(config Config) (*Server, error) {
var webhooksConfig []webhooks.Config
for _, c := range config.Webhooks {
webhooksConfig = append(webhooksConfig, webhooks.Config{Channel: c.Channel, Event: c.Event, Kind: c.Kind, WorkspaceRegex: c.WorkspaceRegex})
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's use a multiline config for the struct here since this line is getting long

return ret0, ret1
}

func (mock *MockSlackClient) PostMessage(channel string, result ApplyResult) error {
Copy link
Contributor Author

@nicholas-wu-hs nicholas-wu-hs Nov 14, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the generated code from pegomock doesn't compile because it doesn't know what ApplyResult is. (same for mock_webhooks.go)
I tried adding in the package manually but then got errors about pegomock.GetGenericMockFrom(mock).Invoke return type is incompatible.
Still figuring this out!

@nicholas-wu-hs
Copy link
Contributor Author

nicholas-wu-hs commented Nov 14, 2017

Still figuring out mocks and adding more tests to slack_test.go, but so far I've

  • changed color => colour, move red, green to constants
  • fixed mangager => manager
  • removed redundant comment
  • added log message for most tests (still WIP)
  • added nil checks before err.Error() in tests
  • tested for whole strings for event and kind
  • refactored slack.go so it can be mocked

I'll ask for a review after I'm done but wouldn't mind another review :)

failureColour = "danger"
)

//go:generate pegomock generate --use-experimental-model-gen --package mocks -o mocks/mock_slack.go slack.go
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

change slack.go to SlackClient. Follow method 2. here: https://github.com/petergtz/pegomock#generating-mocks

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wow, thank you ahahaha, that fixed the can't find ApplyResult
Now I'm looking into

mocks/mock_slack.go:50:9: no new variables on left side of :=
mocks/mock_slack.go:50:9: cannot use pegomock.GetGenericMockFrom(mock).Invoke("PostMessage", params, []reflect.Type literal) (type pegomock.ReturnValues) as type webhooks.ApplyResult in assignment
mocks/mock_slack.go:52:8: invalid argument result (type webhooks.ApplyResult) for len
mocks/mock_slack.go:53:12: invalid operation: result[0] (type webhooks.ApplyResult does not support indexing)
mocks/mock_slack.go:54:17: invalid operation: result[0] (type webhooks.ApplyResult does not support indexing)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as you mentioned, this was due to variable name clashing so I changed result to applyResult on the interface, thanks for catching this :)

const SlackKind = "slack"
const ApplyEvent = "apply"

//go:generate pegomock generate --use-experimental-model-gen --package mocks -o mocks/mock_webhooks.go webhooks.go
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

change webhooks.go to WebhooksSender

@nicholas-wu-hs
Copy link
Contributor Author

nicholas-wu-hs commented Nov 15, 2017

  • fixed the mocks by generating from interfaces instead of files (per your feedback)
  • modified tests to use mock slack client
  • added tests to slack_test.go, all tests are passing

@lkysow
Copy link
Collaborator

lkysow commented Nov 15, 2017

Your commit isn't passing gometalint.

# gotype and gotypex are disabled because they don't pass on CI and https://github.com/alecthomas/gometalinter/issues/206
# gocyclo is temporarily disabled because we don't pass it right now
# golint is temporarily disabled because we need to add comments everywhere first
gometalinter --disable gotype --disable gotypex --disable=gocyclo --disable golint --enable=megacheck --enable=unparam --deadline=120s --vendor -t --line-length=120 $(find . -type f -name '*.go' ! -path "./vendor/*" ! -path "./server/static/bindata_assetfs.go" ! -path "**/mocks/*" | xargs -I '{}' dirname '{}' | sort -u)
server/events/webhooks/slack_test.go:29:11:warning: error return value not checked (hook.Send(result)) (errcheck)
Makefile:51: recipe for target 'gometalint' failed

You can run the make target locally to test.

@nicholas-wu-hs
Copy link
Contributor Author

nicholas-wu-hs commented Nov 15, 2017

I see! I think I know the problem from the error message, will add in the fixes.
EDIT: fixed it in b17eec2, just found out you can click details to see what failed haha

Should I add in the vendor dependencies (i.e. the slack library) as part of this pull request?

failureColour = "danger"
)

//go:generate pegomock generate --use-experimental-model-gen --package mocks -o mocks/mock_slack.go SlackClient
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use mocks/mocks_slack_client.go to match the interface name rather than the name of this file. That way, if we generate a mock for more than one interface in this file, it won't delete the previously generated file (which would have the same name)

PostMessage(channel string, applyResult ApplyResult) error
}

type ConcreteSlackClient struct {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've decided to go with Default for the actual implementations of interfaces, so call this DefaultSlackClient. Still need to fix a bunch of the other stuff but may as well start here.

}
}

func NewSlack(r *regexp.Regexp, channel string, client SlackClient) (*SlackWebhook, error) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

split the slack client and webhook into two files slack_client.go and slack.go


type SlackClient interface {
AuthTest() error
ChannelExist(channelName string) (bool, error)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

call this ChannelExists so it reads properly

RegisterMockTestingT(t)
client := mocks.NewMockSlackClient()
regex, err := regexp.Compile(".*")
Ok(t, err)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice

Environment: "production",
}
err = hook.Send(result)
Ok(t, err)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

have you tried commenting out the line where you check if the regex matches (in the actual implementation) and seeing if this test passes (I think it will). You need to check that PostMessage was never called. I think you can do something easy like client.VerifyWasCalled(Never()).PostMessage(...) to ensure it is never called.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, you're correct, the test passes commenting out the regex match.
client.VerifyWasCalled(Never()).PostMessage(...) works and the test fails if i comment out the regex match! ty ty

Also renamed Concrete -> Default, Exist -> Exists
@nicholas-wu-hs nicholas-wu-hs force-pushed the slack-integration branch 3 times, most recently from 1b67e1d to 5d9ebaa Compare November 17, 2017 02:13
@nicholas-wu-hs
Copy link
Contributor Author

Finished writing the tests

@lkysow
Copy link
Collaborator

lkysow commented Nov 17, 2017

Merged in #199!

@lkysow lkysow closed this Nov 17, 2017
@nicholas-wu-hs nicholas-wu-hs deleted the slack-integration branch November 20, 2017 21:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants